在第10天,我們終於學習了一個關鍵的響應式概念:從現有狀態衍生新狀態。在 Vue 3 與 Angular 中,使用 computed 函數來從其他狀態創建只讀的響應式狀態。這些狀態可以是只讀或可寫的狀態。在 Svelte 5 中,衍生狀態透過 $derived 或 $derived.by rune 創建。
以下是三個衍生狀態的示例:
首先,從 vue 中導入 computed,reverse_items 是透過複製 items ref 並反轉來建立的 computed。
在模板中,使用 v-if 指令檢查 reversed_items 的長度是否大於零。條件為真時,使用 v-for 指令遍歷 reversed_items,由新至舊顯示項目。
<script setup lang="ts">
import { ref, computed } from 'vue'
const items = ref<Item[]>([])
const reverse_items = computed(() => [...items.value].reverse())
</script>
<template v-if="reversed_items.length > 0">
    <ul>
      <div class="list-item" v-for="item in reverse_items" :key="item.id">
        <li>{{ item.id }} - {{ item.label }}
        </li>
      </div>
    </ul>
</template>
使用 $derived rune 取得反轉 items 列表並指派給 reversed_items。模板使用 #if 檢查 reversed_items 非空,然後用 #each 由新至舊遍歷項目。
<script lang="ts">
    let items = $state([] as Item[]);
    let reversed_items = $derived([...items].reverse());
</script>
{#if reversed_items.length > 0}
<ul>
   {#each reversed_items as item (item.id)}
       <div class="list-item">
          <li>{item.id} - {item.label}</li>
       </div>
    {/each}
</ul>
{/if}
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
@Component({
  selector: 'app-shopping-cart',
  imports: [FormsModule, NgIcon],
  template: `
    @if (reverse_items().length > 0) {
      <ul>
        @for (item of reverse_items(); track item.id) {
          <div class="list-item">
            <li>{{ item.id }} - {{ item.label }}</li>
          </div>
        }
      </ul>
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartComponent {
  items = signal<Item[]>([]);
  reverse_items = computed(() => [...this.items()].reverse());
}
從 @angular/core 導入 computed。reverse_items 的定義與 Vue 3 相同,模板透過 @if 判斷 reverse_items 是否有元素,條件為真時,以 @for 逐一由新至舊顯示。
計算已購買商品的數量,可以通過 computed 中使用 Array.reduce 來完成,統計 purchased 屬性為 true 的項目數。
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ref, computed } from 'vue'
const items = ref<Item[]>([])
const num_items_purchased = computed(() =>
  items.value.reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0),
)
<script lang="ts">
    let items = $state([] as Item[]);
    let num_items_purchased = $derived(
        items.reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0)
    );
</script>
num_items_purchased rune 是透過在 $derived rune 中調用 Array.reduce 來衍生出來的。"
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
@Component({
  selector: 'app-shopping-cart',
  template: `
     ...
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartComponent {
  items = signal<Item[]>([]);
  num_items_purchased = computed(() => this.items().reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0));
}
num_items_purchased 是一個 computed signal,用來表示已購買商品的數量。透過迭代 items signal,計算出 purchased 屬性為 true 的項目數量。
num_items_purchased_label 是從 num_items_purchased 衍生出來的顯示字串。
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ref, computed } from 'vue'
const items = ref<Item[]>([])
const num_items_purchased = computed(() =>
  items.value.reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0),
)
const num_items_purchased_label = computed(() => {
  const unit = num_items_purchased.value === 1 ? 'item' : 'items'
  return `${num_items_purchased.value} ${unit} purchased`
})
</script>
當 num_items_purchased 等於 1 時,顯示文字為 "num_items_purchased.value item purchased"。當 num_items_purchased 大於 1 時,顯示文字為 "num_items_purchased.value items purchased"。
<div class="header">
      <template v-if="num_items_purchased > 0 && num_items_purchased < items.length">
        {{ num_items_purchased_label }}</template
      >
      <template v-else-if="num_items_purchased === 0">
          You have not purchased any items yet.
      </template>
      <template v-else>You have bought everything in the shopping cart.</template>
</div>
當 num_items_purchased 在 1 和小於總商品數之間時,模板會顯示 num_items_purchased_label 的值。
當 num_items_purchased 是 0 時,模板會顯示 "You have not purchased any item yet."。
當 num_items_purchased 等於總商品數時,模板會顯示 "You have bought everything in the shopping cart."。
num_items_purchased_label 無法用 $derived,因此我們使用 $derived.by 來建立一個衍生的 rune。
<script lang="ts">
    let items = $state([] as Item[]);
    let num_items_purchased = $derived(
        items.reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0)
    );
    let num_items_purchased_label = $derived.by(() => {
        const unit = num_items_purchased > 1 ? 'items' : 'item';
        return `${num_items_purchased} ${unit} purchased`;
    });
</script>
{#if num_items_purchased > 0 && num_items_purchased < items.length}
    {num_items_purchased_label}
{:else if num_items_purchased == 0}
    You have not purchased any items yet.
{:else}
    You have bought everything in the shopping cart.
{/if}
類似於 Vue 3 應用程式,if-else-if-else 控制流程用於顯示 num_items_purchased_label rune 以及靜態字串。
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
@Component({
  selector: 'app-shopping-cart',
  template: `
      <div class="header">
        @let num = num_items_purchased();
        @if (num > 0 && num < items().length) {
          {{ num_items_purchased_label() }}
        } @else if (num === 0) {
          You have not purchased any items yet.
        } @else {
          You have bought everything in the shopping cart.
        }
      </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartComponent {
  items = signal<Item[]>([]);
  num_items_purchased = computed(() => this.items().reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0));
  num_items_purchased_label = computed(() => {
    const unit = this.num_items_purchased() === 1 ? 'item' : 'items';
    return `${this.num_items_purchased()} ${unit} purchased`;
  });
}
num_items_purchased 值在控制流程中出現了三次。因此,我們將其重構為一個 num 變數。當 num 在 1 和小於總商品數之間時,會顯示 num_items_purchased_label 的值。
當 num 為 0 時,顯示 "You have not purchased any item yet"。
當 num 等於總項目數時,顯示 "You have bought everything in the shopping cart."。